enum Option[+A]:
case Some(get: A)
case None
昨天 那些瞎 g8 問題的解法就是讓那些 function 的回傳變成一種若有似無的型態,就像 薛丁格的貓 那樣,
Option 有 2 種表示,一種代表定義存在的 Some
,另一種是未定義的 None
,我們可以用 Option 來幫前一天的 mean function 加工,讓 mean 的回傳能包含所有造成它無法定義的狀況。
def mean(l: List[Double]): Option[Double] =
if l.isEmpty then
None
else
Some(l.sum / l.length)
雖然 Scala 原生庫中已經有 Option 了,但我們還是可以試著自行實現看看,了解如何使用 pattern match 和 high-order function 去實作那些符合 functional programming 風格的好用功能;
但今天的實作會跟 Day 3 ~ Day 5 有些不太一樣,我們是把所有 function 定義在 List 的 companion object 裡,而今天我會在 Option 這個 enum 底下定義 function,如此能直接以 Option 物件做 function 呼叫,使用上更直覺。
Exercise D7-1
嘗試使用 pattern match 實作以下 function 吧!
getOrElse
:從 Option 取得值,或回傳 default 值。flatMap
:能把 2 個 Option 弄成一個 Option 後回傳。orElse
:若 Option 有被定義,則回傳自己,否則回傳入參 Option。filter
:只保留符合 function f
條件 的 Option。enum Option[+A]:
case Some(get: A)
case None
def map[B](f: A => B): Option[B] = this match
case None => None
case Some(x) => Some(f(x))
def getOrElse[B >: A](default: => B): B
def flatMap[B](f: A => Option[B]): Option[B]
def orElse[B >: A](ob: => Option[B]): Option[B]
def filter(f: A => Boolean): Option[A]
default: => B
表示了 default 是 call-by-name,然後回傳 B;deafult 如果用不到的話,它就不會被 evaluate,我們在 Day 9 的 Laziness 時會介紹更多。
用 Option 包裹的好處就是我們能用 high-order function 去做各種轉換操作,而不用在其中處理錯誤,我們來看個例子,
scala> case class Employee(name: String, department: String)
scala> val employees = List(Employee("tshine73", "RD"), Employee("Bob", "Support"))
scala> employees.find(_.name == "Bob")
| .map(_.department)
| .filter(_ == "Support")
| .getOrElse("default department")
val res1: String = Support
我定義了一個 Employee 類別,然後使用 List.find
嘗試找到名為 Bob 的員工,find 會回傳 Option 物件,
def find(p: A => Boolean): Option[A]
然後透過 map
將 Employee Option 轉變為部門名稱,然後在使用 filter
過濾部門是否為 Support,否則就回傳預設部門名稱;
當其中某些操作失敗時,後續的所有操作都不會執行,例如 None.map(f)
就會立即回傳 None,直到你使用 getOrElse
之類的 function 去取值;
scala> employees.find(_.name == "tshine73")
| .map(_.department)
| .filter(_ == "Support")
| .getOrElse("default department")
val res2: String = default department
當你還是需要將 None 轉為 Exception 時,一個常用的方式是 getOrElse(throw new Exception("Fail"))
。
或許你有注意到,Option 無法讓調用者知道究竟是出了哪些錯,我們只看到 None 被傳遞回來,但有些時候我們想知道到底發生什麼錯誤,此時我們有另一個選擇 Either,
enum Either[+E, +A]:
case Left(get: E)
case Right(get: A)
跟 Option 的最大的不同是,2 種表示都有攜帶值,Left
通常表達失敗或發生錯誤,而 Right
通常表達成功,
讓我們來看如何用 Either 改寫這幾天用到的 mean function,現在我們可以透過 Left 來取得錯誤資訊了。
def mean(xs: Seq[Double]): Either[Exception, Double] =
if xs.isEmpty then
Left(new ArithmeticException("mean of empty list"))
else
Right(xs.sum / xs.length)
Exercise D7-2
跟 Option 類似,我們一樣可以用 pattern match 在 Either 中實作 flatMap, orElse
的 function,來試看看吧!
enum Either[+E, +A]:
case Left(get: E)
case Right(get: A)
def map[B](f: A => B): Either[E, B] = this match
case Left(e) => Left(e)
case Right(a) => Right(f(a))
def flatMap[EE >: E, B](f: A => Either[EE, B])
def orElse[EE >: E, B >: A](b: => Either[EE, B])
明天要來分享如何讓既有的 function 支持 Option 或 Either。